今天要接著來看,是如何設定各式各樣的 route 了。先來看 get()
與 post()
的原始碼:
public function get($uri, $action = null)
{
return $this->addRoute(['GET', 'HEAD'], $uri, $action);
}
public function post($uri, $action = null)
{
return $this->addRoute('POST', $uri, $action);
}
這裡可以知道,實際做事的是 addRoute()
:
public function addRoute($methods, $uri, $action)
{
return $this->routes->add($this->createRoute($methods, $uri, $action));
}
RouteCollection::add()
是新增一筆 Route 並把該 Route 實例回傳。這樣的設計是為了讓後面的 fluent pattern 可以更加直觀:
Route::get('/', function() {
return 'whatever';
})->name('some-name');
createRoute()
則是建立 Route 實例:
protected function createRoute($methods, $uri, $action)
{
// 如果 $action 是 controller@method 的寫法的話,把它轉成 Controller Action
if ($this->actionReferencesController($action)) {
$action = $this->convertToControllerAction($action);
}
// 產生實例
$route = $this->newRoute(
$methods, $this->prefix($uri), $action
);
// 如果昨天的 groupStack 有東西,就將設定寫到 route 裡
if ($this->hasGroupStack()) {
$this->mergeGroupAttributesIntoRoute($route);
}
// 把 where 設定寫入 route
$this->addWhereClausesToRoute($route);
return $route;
}
順帶一提,可以發現 Laravel 在撰寫主要邏輯時,都會寫的比較白話;當追查後面實作才會覺得比較「技術」。
actionReferencesController()
的任務是確認 Action 是不是指向 Controller:
protected function actionReferencesController($action)
{
// 當不是 Closure ,而且是 string 或者是 ['uses' => string] 的話,就假定它是 Controller
if (! $action instanceof Closure) {
return is_string($action) || (isset($action['uses']) && is_string($action['uses']));
}
return false;
}
轉換成 array 是呼叫 convertToControllerAction()
:
protected function convertToControllerAction($action)
{
// 最終還是轉 array
if (is_string($action)) {
$action = ['uses' => $action];
}
// 如果 group stack 有東西的話,那 group 設定的 namespace 必須要加到 Controller 的前面
if (! empty($this->groupStack)) {
$action['uses'] = $this->prependGroupNamespace($action['uses']);
}
// 複製一份給 controller key
$action['controller'] = $action['uses'];
return $action;
}
prependGroupNamespace()
裡面有一段判斷可以看一鑑:
return isset($group['namespace']) && strpos($class, '\\') !== 0
? $group['namespace'].'\\'.$class : $class;
isset($group['namespace'])
應該不用多解釋,有趣的地方是 strpos($class, '\\') !== 0
,這代表如果 Controller 給絕對路徑的 namespace 的話,是不會受到 group 的 namespace 影響的。假設我有一個 Controller,完整類別名為 \App\Http\Controllers\HelloController
則下面兩個是等價的:
文件其實沒有講到這個用法,要翻原始碼才會知道
// 假設預設 namespace 是 App\Http\Controllers
Route::get('foo', 'HelloController@method');
Route::get('foo', '\App\Http\Controllers\HelloController@method');
準備好 Action 後,就可以建構 Route 實例了。建構完 Route,會確認有沒有 group stack,有的話就呼叫 $this->mergeGroupAttributesIntoRoute()
合併 group stack 設定到 Route 實例裡
protected function mergeGroupAttributesIntoRoute($route)
{
// 先取得既有 Action 再跟最後一個 group 設定合併,然後再設定回 Route 實例
$route->setAction($this->mergeWithLastGroup($route->getAction()));
}
最後,呼叫 addWhereClausesToRoute()
把 global where 設定加進 Route 實例裡。
protected function addWhereClausesToRoute($route)
{
$route->where(array_merge(
$this->patterns, $route->getAction()['where'] ?? []
));
return $route;
}
這裡其實就是實作 Global Constraints 的功能。為何下面的 pattern()
設定會變成全域有效,就是上面這段程式碼實作出來的。
Route::pattern('id', '[0-9]+');
Route::get('user/{id}/{name}', function ($id, $name) {
//
})->where(['name' => '[a-z]+']);
同時也可以知道 Route 所設定的 where 是 array 多筆的形式。
到目前為止,Router 跟設定有關的程式碼都看的差不多了,我們可以發現 Router 本身跟設定相關最核心的任務,其實就是產生 Action 並建構 Route 實例。而 Route 實例也不存放在 Router,而是放在 RouteCollection。這就是標準的 facade pattern,所有控制後面角色行為的互動,都可以靠 facade,也就是 Router 來處理;而且,facade pattern 的一個特色是,因為這三天已經知道必要的參數是哪些了,所以是可以繞過 Router 自己來處理的。
明天再來繼續看 Route 如何存放與處理 Action 的資訊。